---
title: API Routes
description: Learn how to create server endpoints with Expo Router.
---

> **warning** API Routes is an experimental feature. Available from Expo Router v3.

import { FileTree } from '~/ui/components/FileTree';
import { Terminal } from '~/ui/components/Snippet';
import { Step } from '~/ui/components/Step';
import { Tab, Tabs } from '~/ui/components/Tabs';

Expo Router enables you to write server code for all platforms, right in your **app** directory.

```json app.json
{
  "web": {
    "bundler": "metro",
    /* @info Output a dynamic server. */
    "output": "server"
    /* @end */
  }
}
```

Server features require a custom Node.js server. Most hosting providers support Node.js, including [Netlify](#netlify), [Cloudflare](https://www.cloudflare.com/), and [Vercel](https://vercel.com).

## What are API Routes

API Routes are functions that are executed when a route is matched. They can be used to handle sensitive data, such as API keys securely, or implement custom server logic. API Routes should be executed in a [WinterCG](https://wintercg.org/)-compliant environment.

API Routes are defined by creating files in the **app** directory with the `+api.ts` extension. For example, the following route handler is executed when the route `/hello` is matched.

<FileTree files={['app/index.tsx', ['app/hello+api.ts', 'API Route']]} />

## Create an API route

<Step label="1">

An API route is created in the **app** directory. For example, add the following route handler. It is executed when the route `/hello` is matched.

```ts app/hello+api.ts
export function GET(request: Request) {
  return Response.json({ hello: 'world' });
}
```

You can export any of the following functions `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, and `OPTIONS` from a server route. The function executes when the corresponding HTTP method is matched. Unsupported methods will automatically return `405: Method not allowed`.

</Step>

<Step label="2">

Start the development server with Expo CLI:

<Terminal cmd={['$ npx expo']} />

</Step>

<Step label="3">

You can make a network request to the route to access the data. Run the following command to test the route:

<Terminal cmd={['$ curl http://localhost:8081/hello']} />

You can also make a request from the client code:

```tsx app/index.tsx
import { Button } from 'react-native';

async function fetchHello() {
  const response = await fetch('/hello');
  const data = await response.json();
  alert('Hello ' + data.hello);
}

export default function App() {
  return <Button onPress={() => fetchHello()} title="Fetch hello" />;
}
```

This won't work by default on native as `/hello` does not provide an origin URL. You can configure the origin URL in the app config file. It can be a mock URL in development. For example:

```json app.json
{
  "plugins": [
    [
      "expo-router",
      {
        /* @info The URL where the API routes are hosted. */
        "origin": "https://evanbacon.dev/"
        /* @end */
      }
    ]
  ]
}
```

</Step>

<Step label="4">

Deploy the website and server to a [hosting provider](#deployment) to access the routes in production on both native and web.

</Step>

> **warning** API route filenames cannot have platform-specific extensions. For example, **hello+api.web.ts** will not work.

## Requests

Requests use the global, standard [`Request`](https://fetch.spec.whatwg.org/#request) object.

```ts app/blog/[post]+api.ts
export async function GET(request: Request, { post }: Record<string, string>) {
  // const postId = request.expoUrl.searchParams.get('post')
  // fetch data for 'post'
  return Response.json({ ... });
}
```

### Request body

Use the `request.json()` function to access the request body. It automatically parses the body and returns the result.

```ts app/validate+api.ts
export async function POST(request: Request) {
  const body = await request.json();

  return Response.json({ ... });
}
```

## Response

Responses use the global, standard [`Response`](https://fetch.spec.whatwg.org/#response) object.

```ts app/demo+api.ts
export function GET() {
  return Response.json({ hello: 'universe' });
}
```

## Errors

You can respond to server errors by using the `Response` object.

```ts app/blog/[post].ts
import { Request, Response } from 'expo-router/server';

export async function GET(request: Request, { post }: Record<string, string>) {
  if (!post) {
    return new Response('No post found', {
      status: 404,
      headers: {
        'Content-Type': 'text/plain',
      },
    });
  }
  // fetch data for `post`
  return Response.json({ ... });
}
```

Making requests with an undefined method will automatically return `405: Method not allowed`. If an error is thrown during the request, it will automatically return `500: Internal server error`.

## Bundling

API Routes are bundled with Expo CLI and [Metro bundler](/guides/customizing-metro). They have access to all of the language features as your client code:

- [TypeScript](/guides/typescript) &mdash; types and [**tsconfig.json** paths](/guides/typescript/#path-aliases-optional).
- [Environment variables](/guides/environment-variables) &mdash; server routes have access to all environment variables, not just the ones prefixed with `EXPO_PUBLIC_`.
- Node.js standard library &mdash; ensure that you are using the correct version of Node.js locally for your server environment.
- **babel.config.js** and **metro.config.js** support &mdash; settings work across both client and server code.

{/* TODO: SSR, redirects, middleware, and so on. */}

## Security

Route handlers are executed in a sandboxed environment that is isolated from the client code. It means you can safely store sensitive data in the route handlers without exposing it to the client.

- Client code that imports code with a secret is included in the client bundle. It applies to **all files** in the **app directory** even though they are not a route handler file (such as suffixed with **+api.ts**).
- If the secret is in a **&lt;...&gt;+api.ts** file, it is not included in the client bundle. It applies to all files that are imported in the route handler.
- The secret stripping takes place in `expo/metro-config` and requires it to be used in the **metro.config.js**.

## Deployment

> **warning** This is experimental and subject to breaking changes. We have no continuous tests against this configuration.

Every cloud hosting provider needs a custom adapter to support the Expo server runtime. The following third-party providers have unofficial or experimental support from the Expo team.

Before deploying to these providers, it may be good to be familiar with the basics of [`npx expo export`](/more/expo-cli#exporting) command:

- **/dist** is the default export directory for Expo CLI.
- Files in **/public** are copied to **/dist** on export.
- The `@expo/server` package is included with `expo` and delegates requests to the server routes.
- `@expo/server` does **not** inflate environment variables from **.env** files. They are expected to load either by the hosting provider or the user.
- Metro is not included in the server.

### Express

<Step label="1">

Install the required dependencies:

<Terminal cmd={['$ npm i -D express compression morgan']} />

</Step>

<Step label="2">

Export the website for production:

<Terminal cmd={['$ npx expo export -p web']} />

</Step>

<Step label="3">

Write a server entry file that serves the static files and delegates requests to the server routes:

```ts server.ts
#!/usr/bin/env node

const path = require('path');
const { createRequestHandler } = require('@expo/server/adapter/express');

const express = require('express');
const compression = require('compression');
const morgan = require('morgan');

const CLIENT_BUILD_DIR = path.join(process.cwd(), 'dist/client');
const SERVER_BUILD_DIR = path.join(process.cwd(), 'dist/server');

const app = express();

app.use(compression());

// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header
app.disable('x-powered-by');

process.env.NODE_ENV = 'production';

app.use(
  express.static(CLIENT_BUILD_DIR, {
    maxAge: '1h',
    extensions: ['html'],
  })
);

app.use(morgan('tiny'));

app.all(
  '*',
  createRequestHandler({
    build: SERVER_BUILD_DIR,
  })
);
const port = process.env.PORT || 3000;

app.listen(port, () => {
  console.log(`Express server listening on port ${port}`);
});
```

</Step>

<Step label="4">

Start the server with `node` command:

<Terminal cmd={['$ node server.ts']} />

</Step>

### Netlify

> **warning** This is experimental and subject to breaking changes. We have no continuous tests against this configuration.

<Step label="1">

Create a server entry file. All requests will be delegated through this middleware. The exact file location is important.

```ts netlify/functions/server.ts
const { createRequestHandler } = require('@expo/server/adapter/netlify');

const handler = createRequestHandler({
  /* @info Points to the root `dist/` (output) folder */
  build: require('path').join(__dirname, '../../dist/server'),
  /* @end */
});

module.exports = { handler };
```

</Step>

<Step label="2">

Create a Netlify configuration file at the root of your project to redirect all requests to the server function.

```yaml netlify.toml
[build]
  command = "expo export -p web"
  functions = "netlify/functions"
  publish = "dist/client"

[[redirects]]
  from = "/*"
  to = "/.netlify/functions/server"
  status = 404

[functions]
  # Include everything to ensure dynamic routes can be used.
  included_files = ["dist/server/**/*"]

[[headers]]
  for = "/dist/server/_expo/functions/*"
  [headers.values]
    # Set to 60 seconds as an example.
    "Cache-Control" = "public, max-age=60, s-maxage=60"
```

</Step>

<Step label="3">

After you have created the configuration files, you can build the website and functions with Expo CLI:

<Terminal cmd={['$ npx expo export -p web']} />

</Step>

<Step label="4">

Deploy to Netlify with the [Netlify CLI](https://docs.netlify.com/cli/get-started/).

<Terminal
  cmd={[
    '# Install the Netlify CLI globally if needed.',
    '$ npm install netlify-cli -g',
    '# Deploy the website.',
    '$ netlify deploy',
  ]}
/>

You can now visit your website at the URL provided by Netlify CLI. Running `netlify deploy --prod` will publish to the production URL.

</Step>

<Step label="5">

If you're using any environment variables or **.env** files, add them to Netlify. You can do this by going to the **Site settings** and adding them to the **Build & deploy** section.

</Step>

### Vercel

> **warning** This is experimental and subject to breaking changes. We have no continuous tests against this configuration.

<Step label="1">

Create a server entry file. All requests will be delegated through this middleware. The exact file location is important.

```ts api/index.ts
const { createRequestHandler } = require('@expo/server/adapter/vercel');

module.exports = createRequestHandler({
  /* @info Points to the root `dist/` (output) folder */
  build: require('path').join(__dirname, '../dist/server'),
  /* @end */
});
```

</Step>

<Step label="2">

Create a Vercel configuration file (**vercel.json**) at the root of your project to redirect all requests to the server function.

<Tabs>
<Tab label="vercel.json v2">

```json vercel.json
{
  "version": 2,
  "outputDirectory": "dist",
  "builds": [
    {
      "src": "package.json",
      "use": "@vercel/static-build",
      "config": {
        "distDir": "dist/client"
      }
    },
    {
      "src": "api/index.ts",
      "use": "@vercel/node",
      "config": {
        "includeFiles": ["dist/server/**"]
      }
    }
  ],
  "routes": [
    {
      "handle": "filesystem"
    },
    {
      "src": "/(.*)",
      "dest": "/api/index.ts"
    }
  ]
}
```

The **legacy** version of the **vercel.json** needs a `@vercel/static-build` runtime to serve your assets from the **dist/client** output directory.

</Tab>
<Tab label="vercel.json v3">

```json vercel.json
{
  "buildCommand": "expo export -p web",
  "outputDirectory": "dist/client",
  "functions": {
    "api/index.ts": {
      "runtime": "@vercel/node@3.0.11",
      "includeFiles": "dist/server/**"
    }
  },
  "rewrites": [
    {
      "source": "/(.*)",
      "destination": "/api/index.ts"
    }
  ]
}
```

The newer version of the **vercel.json** does not use `routes` and `builds` configuration options anymore, and serves your public assets from the **dist/client** output directory automatically.

</Tab>
</Tabs>

</Step>

<Step label="3">

> **Note:** This step only applies to users of the **legacy** version of the **vercel.json**. If you're using v3, you can skip this step.

After you have created the configuration files, add a `vercel-build` script to your **package.json** file and set it to `expo export -p web`.

</Step>

<Step label="4">

Deploy to Vercel with the [Vercel CLI](https://vercel.com/docs/cli).

<Terminal
  cmd={[
    '# Install the Vercel CLI globally if needed.',
    '$ npm install vercel -g',
    '# Deploy the website.',
    '$ vercel deploy',
  ]}
/>

You can now visit your website at the URL provided by the Vercel CLI.

</Step>

## Known limitations

Several known features are not currently supported in the API Routes beta release.

### No dynamic imports

API Routes currently work by bundling all code (minus the Node.js built-ins) into a single file. This means that you cannot use any external dependencies that are not bundled with the server. For example, a library such as `sharp`, which includes multiple platform binaries, cannot be used. This will be addressed in a future version.

### ESM not supported

The current bundling implementation opts to be more unified than flexible. This means the limitation of native not supporting ESM is carried over to API Routes. All code will be transpiled down to Common JS (`require`/`module.exports`). However, we recommend you write API Routes using ESM regardless. This will be addressed in a future version.
